🎮 https://lucy-zhang-odo.github.io/ironman-js-battle/
不用安裝、手機也能玩。原始碼在 GitHub。
30 天前我一行 JavaScript 都沒寫過。
| 階段 | 內容 |
|---|---|
| Day 1–12 | 變數、型別、字串、判斷、陣列、物件、迴圈、函式、作用域、DOM、事件 |
| Day 13–21 | XSS 與轉義、CSS 操作、表單驗證、亂數、計時器與事件迴圈、狀態管理、非同步、localStorage、除錯 |
| Day 22–30 | 專案結構、戰鬥系統、數值平衡、CSS 動畫、增益系統、RWD、部署 |
但真正留下來的不是語法清單,是幾個反覆出現的觀念:
兩個入口都在同一個地方:名字輸入框。
整個系列裡,那個框看起來只是拿來取名字的。實際上它是兩個秘密的鑰匙。
把名字輸入成 魔王(或 BOSS、大魔王、魔王大人),然後出征。
畫面會整個沉下去:暗色主題、標題變成「魔王 vs 勇者 —— 逆位」、血條從綠紅變成紫紅與冷藍,戰鬥紀錄冒出一行螢光綠的字:
……有人在呼喚黑暗。
然後你就是魔王,勇者來討伐你。
而且數值也對調了 —— 你變成血少(60)但攻高(15)的那一方,對手是血厚(100)但攻低(9)的討伐者。這比正常模式難,因為你的容錯空間小得多。紀錄也存在不同的地方,不會污染正常模式的成績。
這個想法是我在 Day 22 規劃最終作品時冒出來的:整個系列都在打小怪,如果有人發現自己也能當魔王,應該會很有趣。
這個的來歷比較特別 —— 它原本是一個真的資安漏洞。
Day 15 我加上名字輸入框之後,自己測出一個問題:在名字裡打 <b>hl</b>,戰鬥訊息裡的 hl 真的變成了粗體。標籤被解析了,那就是 XSS。
當時我用轉義把它堵起來,並且在文章最後留了一句:
這個漏洞其實還有另一種處理方式,不是把它堵起來,而是把它變成一個功能。我留到系列最後一天再說。
就是現在。試試看輸入:
[red]小明[/red]
[b]勇者[/b]
[blue]水[/blue][red]火[/red]
名字會在血條旁、戰鬥紀錄、結果畫面三個地方都套用樣式。
因為順序:先把所有真的 HTML 轉義掉,再把「我自己定義的標記」還原成標籤。
function styleName(raw) {
let s = escapeHtml(raw); // 第一步:攻擊在這裡就全死了
for (const mark in NAME_TAGS) {
s = s.replaceAll(mark, NAME_TAGS[mark]); // 第二步:只還原白名單裡的
}
return s;
}
這叫白名單:不是「禁止我想得到的壞東西」,而是「只允許我列出的東西」。黑名單永遠會漏,白名單只會過於嚴格 —— 而過於嚴格是可以接受的。
所以輸入 <img src=x onerror="alert(1)"> 還是會原樣顯示成一串文字,一點事都不會發生;但 [red] 會變成紅色。玩家拿不到我沒給的東西。
這是論壇用了二十幾年的老方法。而我在 Day 15 把它記成待辦事項的時候,只知道「有這種做法」,今天才真的做出來。
名字限制 10 個字,但 [red]小明[/red] 有 13 個字元。所以算長度時要先把標記拿掉:
function plainName(raw) { ... } // 去掉標記,只算實際的名字
不然玩家加個顏色就沒名字可以取了。這種「功能和限制互相打架」的地方,不自己玩一次不會發現。
這是我沒預期到的收穫。
兩個彩蛋加起來,逼出了五個 bug,而且它們全部是同一個家族:
| # | 哪裡 | 症狀 |
|---|---|---|
| 1 | 標題寫死在 HTML | 逆位模式標題還是「勇者 vs 小怪」 |
| 2 | 骰子說明寫死「小怪」 | 逆位模式下訊息說錯名字 |
| 3 | 按鈕 hover 底色寫死淺色 | 暗色模式下按鈕整顆反白 |
| 4 | 只設了背景色,沒設文字色 | 暗色模式下輸入框的字看不見 |
| 5 | 設定裡寫死的名字蓋掉玩家輸入 | 樣式標記被丟掉 |
全部都是「在原本的前提下正確,前提一變就出錯」。
第 4 個特別微妙 —— 它不是「寫死了值」,而是漏掉了一半:背景和文字顏色必須成對設定,只改一邊,在另一種主題下就會壞。
而這讓我想起 Day 15 那個我最在意的發現:當時我在程式裡留了一行註解寫著「這裡沒有使用者輸入,所以用 innerHTML 是安全的」—— 那句話在寫下的當天是真的,加了輸入框之後就變成謊言了。
寫死的值、過期的註解、只改一半的設定 —— 都是同一件事:假設會過期,但程式碼不會自己提醒你。
彩蛋逼我換一套前提跑一次,那些假設就全部現形了。我原本以為彩蛋只是好玩,結果它是這 30 天裡最有效的一次測試。
這個遊戲的完整構想其實比現在大得多。當初我想做的是:
三十天做不完,所以砍成了「一場戰鬥」。但砍掉的東西我都記著,而且這 30 天累積的東西剛好是它的地基:
| 現在有的 | 未來要擴充成 |
|---|---|
| 一隻小怪 | 十關不同的敵人 |
| 骰子六面 | 道具、技能、裝備 |
| 增益清單 | 完整的 buff / debuff 系統 |
| 本機紀錄 | 雲端英雄榜(需要後端) |
| 單場戰鬥 | 分支路線的關卡流程 |
Day 27 做增益系統的時候我就寫過:「後續的怪物會需要不同的數值,勇者也需要不同裝備,那時候就不能再靠手算兩個數字了。」那是下一個階段的功課。
而雲端英雄榜那部分,Day 20 已經先想清楚了:分數不能存在瀏覽器,要存在伺服器,而且伺服器要自己驗算 —— 因為前端的一切都可以被繞過。
照慣例,最後一篇應該寫一段熱血的話。但我想寫實際發生的事。
這個主題是我自己有興趣、自己想做的。可是邊上班邊做,大概做到第三天(內容做到 Day 16 左右)就開始有點煩躁了。
不是因為題目無聊,而是因為它和「上班」之間的關係:明天還要上班,但今天還要維持更新。有時候晚上睡前才突然想起來這件事,然後手忙腳亂。
會撐下來,有三個原因:
第一,存稿。 我不是每天現寫。有幾個週末一口氣做了好幾天的份量,所以平日只要按發表。如果沒有這件事,我應該撐不到第十天。
第二,題目看得見成果。 選遊戲不是因為它簡單,是因為每天結束的時候畫面上會多一個東西。Day 10 第一次讓數字出現在網頁上、Day 25 第一次看到血條震動 —— 那些瞬間是真的會讓人想繼續。
第三,我是為了薪水開始的。 Day 29 寫過,這裡再說一次:起點是現實的。而現實的理由在你煩躁的時候反而比較耐用,因為它不需要你保持熱情。
所以如果要對一樣在考慮參賽的人說一句話,大概是:別高估自己的熱情,也別低估存稿的重要性。
我還是一名 QA。薪水沒有變,工作沒有變。
但有些東西不一樣了:我看得懂錯誤訊息在說什麼、我知道「沒有紅字不等於沒問題」、我會為了驗證一個數值設計去寫一個跑 1000 場的模擬器,而不是手動測 20 次。
還有一個網址,裡面有一個我從零做出來的遊戲。
它很小。但它是我的。
去玩玩看吧 —— 記得試試看把名字取成「魔王」。
🎮 https://lucy-zhang-odo.github.io/ironman-js-battle/
感謝每一位讀到這裡的人。明年見。